package com.player.dao;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.google.appengine.api.datastore.QueryResultIterable;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Query;
import com.player.entity.Player;
import com.player.entity.Ranking;
import com.player.entity.RankinglistType;
import com.player.rest.exception.PropertyAlreadyExistsException;

public class RankingDAO extends DAO {

	public static final String PROP_POINTS = "points";
	public static final String PROP_RANKINGLIST = "rankinglist";
	public static final String PROP_PLAYER_KEY = "playerKey";

	/* pkg */RankingDAO() {
	}

	public Ranking create(Long playerId, RankinglistType rankinglist) throws PropertyAlreadyExistsException {
		ensureOneRankingPerRankinglist(playerId, rankinglist);
		Ranking ranking = new Ranking(playerId, rankinglist);
		ofy().put(ranking);
		return ranking;
	}

	private void ensureOneRankingPerRankinglist(Long playerId, RankinglistType rankinglist)
			throws PropertyAlreadyExistsException {
		Key<Player> playerKey = new Key<Player>(Player.class, playerId);
		QueryResultIterable<Key<Ranking>> rankings = ofy().query(Ranking.class).filter(PROP_PLAYER_KEY, playerKey)
				.filter(PROP_RANKINGLIST, rankinglist.name()).fetchKeys();
		if (rankings.iterator().hasNext()) {
			StringBuilder message = new StringBuilder();
			message.append("Player (id=");
			message.append(playerId);
			message.append(") is already listed in rankinglist ");
			message.append(rankinglist.name());
			throw new PropertyAlreadyExistsException(message.toString(), PROP_PLAYER_KEY, playerKey.getId());
		}
	}

	public void save(Ranking ranking) {
		ofy().put(ranking);
	}

	public List<Ranking> get(Long playerId) {
		Key<Player> playerKey = new Key<Player>(Player.class, playerId);
		Query<Ranking> result = ofy().query(Ranking.class).filter(PROP_PLAYER_KEY, playerKey);
		return result.list();
	}

	public Ranking get(Long playerId, RankinglistType rankingList) {
		return getInternal(playerId, rankingList, false);
	}

	public Ranking getWithRank(Long playerId, RankinglistType rankingList) {
		return getInternal(playerId, rankingList, true);
	}

	private Ranking getInternal(Long playerId, RankinglistType rankingList, boolean loadRank) {
		Key<Player> playerKey = new Key<Player>(Player.class, playerId);
		Query<Ranking> result = ofy().query(Ranking.class).filter(PROP_PLAYER_KEY, playerKey).filter(PROP_RANKINGLIST,
				rankingList.name());
		List<Ranking> rankings = result.list();
		if (rankings.size() > 1) {
			throw new IllegalStateException("Player (id=" + playerKey.getId()
					+ ") is registered more than once in the same rankinglist (" + rankingList + ")!");
		}

		if (rankings.isEmpty()) {
			return null;
		}

		Ranking ranking = rankings.get(0);

		if (loadRank) {
			loadRank(ranking, rankingList);
		}

		return ranking;
	}

	private void loadRanks(List<Ranking> rankings, RankinglistType rankinglist) {
		Query<Ranking> query = ofy().query(Ranking.class).filter(PROP_RANKINGLIST, rankinglist.name());
		List<Ranking> sortedRankings = sortRankingsByPoints(query.list());
		int foundRankings = 0;
		for (int i = 0; i < sortedRankings.size(); i++) {
			// FIXME improve
			for (Ranking ranking : rankings) {
				if (sortedRankings.get(i).getId() == ranking.getId()) {
					ranking.setRank(i + 1);
					foundRankings++;
					if (foundRankings >= rankings.size()) {
						break;
					}
				}
			}
		}

		if (foundRankings < rankings.size()) {
			throw new IllegalStateException(
					"Could not load all ranks because not all Ranking have been found in rankinglist=" + rankinglist);
		}
	}

	private void loadRank(Ranking ranking, RankinglistType rankinglist) {
		ArrayList<Ranking> list = new ArrayList<Ranking>();
		list.add(ranking);
		loadRanks(list, rankinglist);
		/*
		 * Query<Ranking> query = ofy().query(Ranking.class).filter(PROP_RANKINGLIST, rankinglist.name()); List<Ranking>
		 * sortedRankings = sortRankingsByPoints(query.list()); for (int i = 0; i < sortedRankings.size(); i++) { if
		 * (sortedRankings.get(i).getId() == ranking.getId()) { ranking.setRank(i + 1); break; } }
		 * 
		 * if (ranking.getRank() == 0l) { throw new
		 * IllegalStateException("Could not load rank because no Ranking with id=" + ranking.getId() +
		 * " has been found in rankinglist=" + rankinglist); }
		 */
	}

	/**
	 * @return List of Rankings for the given rankinglist with UNinitialized player
	 */
	public List<Ranking> getAll(RankinglistType rankinglist, boolean loadRank) {
		Query<Ranking> result = ofy().query(Ranking.class).filter(PROP_RANKINGLIST, rankinglist.name());

		List<Ranking> rankings = result.list();

		if (loadRank) {
			List<Ranking> sortedRankings = sortRankingsByPoints(rankings);
			for (int i = 0; i < sortedRankings.size(); i++) {
				sortedRankings.get(i).setRank(i + 1);
			}
		}

		return rankings;
	}

	public List<Ranking> getAllWithPlayers(RankinglistType rankinglist) {
		Query<Ranking> result = ofy().query(Ranking.class).filter(PROP_RANKINGLIST, rankinglist.name());

		// Simple implementation with O(n) calls to the db
		// for (Ranking ranking : result) {
		// Player player = ofy().get(ranking.getPlayerKey());
		// ranking.setPlayer(player);
		// }
		// List<Ranking> sortedRankings = result.list();
		// Collections.sort(rankings, new Comparator<Ranking>() {... }
		// for (int i = 0; i < sortedRankings.size(); i++) {
		// sortedRankings.get(i).setRank(i + 1);
		// }
		// return sortedRankings;
		// END

		int count = result.count();
		Map<Key<Player>, Ranking> playerKeyToRanking = new HashMap<Key<Player>, Ranking>(count);
		for (Ranking ranking : result) {
			playerKeyToRanking.put(ranking.getPlayerKey(), ranking);
		}

		// fetch all referenced players
		Map<Key<Player>, Player> playerKeyToPlayer = ofy().get(playerKeyToRanking.keySet());
		for (Entry<Key<Player>, Player> entry : playerKeyToPlayer.entrySet()) {
			Ranking ranking = playerKeyToRanking.get(entry.getKey());
			ranking.setPlayer(entry.getValue());
		}

		List<Ranking> sortedRankings = sortRankingsByPoints(new ArrayList<Ranking>(playerKeyToRanking.values()));

		for (int i = 0; i < sortedRankings.size(); i++) {
			sortedRankings.get(i).setRank(i + 1);
		}

		return sortedRankings;
	}

	private List<Ranking> sortRankingsByPoints(List<Ranking> rankings) {
		Collections.sort(rankings, new Comparator<Ranking>() {
			@Override
			public int compare(Ranking o1, Ranking o2) {
				if (o1 == null && o2 == null) {
					return 0;
				} else if (o1 == null && o2 != null) {
					return 1;
				} else if (o1 != null && o2 == null) {
					return -1;
				} else {
					if (o1.getPoints() == o2.getPoints()) {
						return 0;
					} else if (o1.getPoints() < o2.getPoints()) {
						return 1;
					} else {
						return -1;
					}
				}
			}
		});
		return rankings;
	}
}
